package hydrap2p.modulemanager;

import hydrap2p.Service;
import hydrap2p.ServiceLocation;
import hydrap2p.helper.ServiceConnector;
import hydrap2p.modulemanager.listen.Listener;
import hydrap2p.modulemanager.listen.Listeners;

import java.io.IOException;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;

import org.apache.thrift.TException;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.transport.TSocket;
import org.apache.thrift.transport.TTransport;
import org.jvnet.winp.WinProcess;

/**
 * Stores the current status of a Module, and associated process information
 * @author whuf
 *
 */

public class ModuleStatusLocal implements Listener, ModuleStatus{

	private ModuleConfig conf;
	
	private ModuleStatusType status;
	private ServiceLocation location;
	
	private final ModuleController parent;
	private final ConfigMunger configger;
	
	private Process child;
	private StreamCopier outcopier;		// thread copying from stdout to my stdout
	private StreamCopier errcopier;		// thread copying from stderr to my stderr

	private static final int KILLTIMEOUT=5000;	// how long shutting down by Thrift can last
	
	public ModuleStatusLocal(ModuleConfig moduleConfig, ModuleController parent, ConfigMunger configger) {
		conf=moduleConfig;
		this.parent=parent;
		this.configger=configger;
		status=ModuleStatusType.STOPPED;
		location=null;
	}
	
	@Override
	public ModuleInfo getInfo()
	{
		ModuleInfo ret = new ModuleInfo();
		ret.setAutostart(conf.autostart);
		ret.setIsRemote(false);
		ret.setName(conf.name);
		ret.setServer("local");
		ret.setRunning(!status.equals(ModuleStatusType.STOPPED));
		
		return ret;
	}
	
	@Override
	public void setAutostart(boolean autostart)
	{
		conf.autostart=autostart;
		configger.updateUserConf(conf);
	}
	@Override
	public boolean getAutostart()
	{
		return conf.autostart;
	}

	@Override
	public ModuleStatusType getStatus() {
		return status;
	}
	
	public void setConfig(ModuleConfig conf) {
		this.conf=conf;
	}

	@Override
	public List<String> providedServices() {
		return conf.services;
	}

	private static <T> String join(final Iterable<T> objs, final String delimiter) {
	    Iterator<T> iter = objs.iterator();
	    if (!iter.hasNext())
	        return "";
	    StringBuffer buffer = new StringBuffer(String.valueOf(iter.next()));
	    while (iter.hasNext())
	        buffer.append(delimiter).append(String.valueOf(iter.next()));
	    return buffer.toString();
	}

	/* (non-Javadoc)
	 * @see hydrap2p.modulemanager.ModuleStatus#start()
	 */
	@Override
	public void start()
	{
		System.out.println("Wanting to start module "+conf.name);
		Listeners.addListener(this);
		synchronized(this)
		{
			if (status==ModuleStatusType.STOPPED)
				status=ModuleStatusType.WAITING;
			else
				return;
		}
		System.out.println("Beginning to start module "+conf.name);
		
		long starttime=System.currentTimeMillis();
		int timeout=50000;
				
		// Start dependent modules
		boolean waiting=true;
		while (waiting)
		{
			List<String> waitingservices=new LinkedList<String>();
			waiting=false;
			for (String other : conf.needs)
			{
				Service.Iface otherservice=ServiceConnector.getService(Service.Client.class, other);
				//ModuleStatus otherstatus=parent.getStatus(other);
				if (otherservice!=null)
				{
					// found it, skip this loop
					continue;
				}
				ModuleStatus otherstatus = parent.getModuleForService(other);
				if (otherstatus==null)
				{
					// can't determine which module to start for necessity
					status=ModuleStatusType.STOPPED;
					System.out.println("Starting "+conf.name+" requires non-existant service "+other);
					return;
				}
				// found the other module to start, try to start it
				ModuleStatusType otherstatustype=otherstatus.getStatus();
				if (otherstatustype==ModuleStatusType.STOPPED)
				{
					waitingservices.add(other);
					System.err.println("Starting parent module "+other);
					parent.start(otherstatus.getInfo().name);
					waiting=true;
				}
				else if (otherstatustype==ModuleStatusType.WAITING)
				{
					waitingservices.add(other);
					waiting=true;
				}
				else if (otherstatustype==ModuleStatusType.STARTED && otherstatus instanceof ModuleStatusLocal && ((ModuleStatusLocal)otherstatus).conf.control!=null)	// waiting for module to register
				{
					waitingservices.add(other);
					waiting=true;
				}
				else if (otherstatustype==ModuleStatusType.STARTEDPLUS)	// module has registered
				{
					waiting |= false;
				}
				else if (otherstatustype==ModuleStatusType.STARTEDEXTER)	// module has registered but we didn't start it
				{
					waiting |= false;
				}
			}
			if (status==ModuleStatusType.STOPPED)
				break;
			
			long nowtime=System.currentTimeMillis();
			if (waiting && starttime+timeout<nowtime)
			{
				status=ModuleStatusType.STOPPED;
				System.out.println("Starting "+conf.name+" timed out waiting for "+join(waitingservices, ", "));
				return;
			}
			if (waiting)
			{
				synchronized(this)
				{
					System.out.println("Starting "+conf.name+" is waiting for "+join(waitingservices, ", "));
					try {
						this.wait(500);
					} catch (InterruptedException e) {
						// Oh well
					}
				}
			}
		}
		if (status==ModuleStatusType.STOPPED)	// while waiting we were told to stop
			return;
				
		synchronized(this)
		{
			try 
			{
				System.out.println("Starting process for module "+conf.name);
				child=Runtime.getRuntime().exec(conf.realcommand);
				outcopier=new StreamCopier(this,"StreamCopier "+conf.name+" stdout",child.getInputStream(),System.out);
				outcopier.start();
				errcopier=new StreamCopier(this,"StreamCopier "+conf.name+" stderr",child.getErrorStream(),System.err);
				errcopier.start();
				

				try {
					int ret=child.exitValue();
					// exitValue will Exception if it's still running
					// if not, it quit
					System.err.println("Module "+conf.name+" quit too quickly");
					return;
				} catch (IllegalThreadStateException e)
				{
					// normal flow
				}
				
				status=ModuleStatusType.STARTED;
			}
			catch (IOException e)
			{
				System.err.println("Failed to start module "+conf.name+": "+e.getMessage());
			}
		}
	}
	
	/* (non-Javadoc)
	 * @see hydrap2p.modulemanager.ModuleStatus#stop()
	 */
	@Override
	public void stop()
	{
		synchronized(this)
		{
			if (status==ModuleStatusType.STOPPED)
				return;
		}
		
		System.out.println("Stopping "+conf.name);
		for (String depends : conf.neededby)
		{
			System.out.println("Stopping dependent "+depends);
			parent.stop(depends);
		}
		System.out.println("Continuing to stop "+conf.name);
			
		synchronized(this)
		{
			status=ModuleStatusType.STOPPED;

			if (location!=null)
			{
				final TTransport transport=new TSocket(location.address,location.port);
				TimerTask emergencykiller = new TimerTask() {
					@Override
					public void run() {
						transport.close();
					}	
				};
				Timer killerrunner=new Timer();
				killerrunner.schedule(emergencykiller, KILLTIMEOUT);
				
				try {
					transport.open();
					ModuleControl.Client connection=new ModuleControl.Client(new TBinaryProtocol(transport));
					System.out.println("Stopping "+conf.name+" through Thrift");
					
					
					connection.stop();
				}
				catch (TException e)
				{
					//System.err.println("Failed to stop "+conf.name+" through Thrift: "+e.getLocalizedMessage());
				}
				killerrunner.cancel();
			}
			else
			{
				System.out.println("Not stopping "+conf.name+" via Thrift, control interface at "+conf.control+" not registered");
			}
			
			if (child!=null)
			{				
				if (System.getProperty("os.name").toLowerCase().contains("win"))
					new WinProcess(child).killRecursively();
				child.destroy();
				System.out.println("Killed "+conf.name);
			}
			System.out.println("Finished "+conf.name);
		}
	}

	/**
	 * A SteamCopier was disconnected, which means the child process died
	 * Make sure we know this
	 */
	public void processDied() {
		if (status!=ModuleStatusType.STOPPED)
		{
			System.err.println("Process for "+conf.name+" died unexpectedly");
			stop();
		}
	}
	
	/**
	 * Inform this ModuleStatus that a service has been registered, check to see if it's us and do something
	 * @param servicename
	 * @param location
	 */
	public void serviceStarted(String servicename, ServiceLocation location)
	{
		if (servicename.equals(this.conf.control))
		{
			System.out.println("Process for "+conf.name+" registered service");
			synchronized(this)
			{
				this.location=location;
				if (status==ModuleStatusType.STARTED)
					status=ModuleStatusType.STARTEDPLUS;
				if (status==ModuleStatusType.STOPPED)
					status=ModuleStatusType.STARTEDEXTER;
			}
			Listeners.updateModule(getInfo());
		}
	}

	/**
	 * Inform this ModuleStatus that a service has been disappeared, check to see if it's us and do something
	 * @param servicename
	 * @param location
	 */
	public void serviceStopped(String servicename)
	{
		if (servicename.equals(this.conf.name))
		{
			System.out.println("Process for "+conf.name+" disappeared");
			this.location=null;
		}
	}

	@Override
	public String toString()
	{
		return "Module status (name:"+conf.name+" status:"+status+")";
	}

	@Override
	public void addModule(ModuleInfo module) {
		// ignored
	}

	@Override
	public void updateModule(ModuleInfo module) {
		synchronized(this)
		{
			if (this.status==ModuleStatusType.WAITING && module.running==true)
			{
				this.notifyAll();		// wake up sleeping startup thread
			}
			if (module.running==false)
			{
				if (conf.needs.contains(module.name) && this.status!=ModuleStatusType.STOPPED)		// a module has shut down that we need
				{
					stop();
				}
			}
		}
	}

	@Override
	public void delModule(ModuleInfo module) {
		// ignored
	}
	


	@Override
	public int hashCode()
	{
		return conf.name.hashCode();
	}
	
	@Override
	public boolean equals(Object obj)
	{
		if (obj instanceof ModuleStatusLocal)
		{
			ModuleStatusLocal other = (ModuleStatusLocal) obj;
			return other.conf.name.equals(this.conf.name);
		}
		return false;
	}
}
